package model

import (
	"encoding/json"
	"fmt"
	"strconv"
	"strings"

	"github.com/iancoleman/orderedmap"

	"github.com/invopop/jsonschema"

	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/bson/bsontype"
)

type (
	Ports []PortWrapper

	PortWrapper struct {
		IntValue    *int32
		StringValue *string
		StructValue *Port
	}

	Port struct {
		Mode      string `json:"mode" bson:"mode"`
		Target    int32  `json:"target" bson:"target"`
		Published int32  `json:"published" bson:"published"`
		Protocol  string `json:"protocol" bson:"protocol"`
	}

	PortConfiguration struct {
		HostPort      int32
		ContainerPort int32
		Protocol      Protocol
	}

	Protocol string
)

const (
	ProtocolTcp = "tcp"
)

func (Ports) JSONSchema() *jsonschema.Schema {
	properties := orderedmap.New()

	properties.Set("mode", jsonschema.Schema{Type: "string"})
	properties.Set("target", jsonschema.Schema{Type: "integer"})
	properties.Set("published", jsonschema.Schema{Type: "integer"})
	properties.Set("protocol", jsonschema.Schema{Type: "string"})

	return &jsonschema.Schema{
		Type: "array",
		Items: &jsonschema.Schema{
			OneOf: []*jsonschema.Schema{
				{Type: "string", Format: "ports"},
				{Type: "number", Format: "ports"},
				{
					Type:                 "object",
					Properties:           properties,
					AdditionalProperties: jsonschema.FalseSchema,
				},
			},
		},
	}
}

func (p *PortWrapper) MarshalJSON() ([]byte, error) {
	if p == nil {
		return []byte("null"), nil
	}

	if p.StructValue != nil {
		return json.Marshal(p.StructValue)
	}

	if p.StringValue != nil {
		return json.Marshal(p.StringValue)
	}

	if p.IntValue != nil {
		return json.Marshal(p.IntValue)
	}

	return []byte("null"), nil
}

func (p *PortWrapper) UnmarshalJSON(bytes []byte) error {
	if string(bytes) == "null" {
		return nil
	}

	for _, char := range string(bytes) {
		switch char {
		case '{':
			result := &Port{}
			err := json.Unmarshal(bytes, result)
			if err != nil {
				return fmt.Errorf("unmarshall to struct port failed: %w", err)
			}

			*p = PortWrapper{
				StructValue: result,
			}
			return nil
		case '"':
			var result string
			err := json.Unmarshal(bytes, &result)
			if err != nil {
				return fmt.Errorf("unmarshall to struct port failed: %w", err)
			}

			*p = PortWrapper{
				StringValue: &result,
			}
			return nil
		default:
			var result int32
			err := json.Unmarshal(bytes, &result)
			if err != nil {
				return fmt.Errorf("unmarshall to struct port failed: %w", err)
			}

			*p = PortWrapper{
				IntValue: &result,
			}
			return nil
		}
	}

	return nil
}

func (p *PortWrapper) MarshalBSONValue() (bsontype.Type, []byte, error) {
	if p == nil {
		return bsontype.Null, []byte{}, nil
	}

	if p.StructValue != nil {
		return bson.MarshalValue(p.StructValue)
	}

	if p.StringValue != nil {
		return bson.MarshalValue(p.StringValue)
	}

	if p.IntValue != nil {
		return bson.MarshalValue(p.IntValue)
	}

	return bsontype.Null, []byte{}, nil
}

func (p *PortWrapper) UnmarshalBSONValue(t bsontype.Type, data []byte) error {
	raw := bson.RawValue{Type: t, Value: data}

	switch t {
	case bsontype.Null:
		*p = PortWrapper{StringValue: nil, IntValue: nil, StructValue: nil}
		return nil
	case bsontype.Int32:
		result := raw.Int32()
		*p = PortWrapper{IntValue: &result}
		return nil
	case bsontype.String:
		result := raw.StringValue()
		*p = PortWrapper{StringValue: &result}
		return nil
	case bsontype.Double:
		result := raw.Double()
		double := fmt.Sprintf("%.0f", result)
		int_, err := strconv.Atoi(double)
		if err != nil {
			return fmt.Errorf("cannot unmarshall port-wrapper type double '%f', '%s': %w", result, double, err)
		}

		int32_ := int32(int_)
		*p = PortWrapper{IntValue: &int32_}
		return nil
	case bsontype.EmbeddedDocument:
		result := Port{}
		if err := raw.Unmarshal(&result); err != nil {
			return fmt.Errorf("cannot unmarshall port: %w", err)
		}
		*p = PortWrapper{StructValue: &result}
		return nil
	default:
		return fmt.Errorf("cannot unmarshall port-wrapper with type '%s'", t)
	}
}

func (p *PortWrapper) ToPortConfiguration() (*PortConfiguration, error) {
	if p.IntValue != nil {
		result := &PortConfiguration{}

		result.HostPort = *p.IntValue
		result.ContainerPort = *p.IntValue
		result.Protocol = ProtocolTcp

		return result, nil
	}

	if p.StringValue != nil {
		result, err := parsePortConfiguration(*p.StringValue)
		if err != nil {
			return nil, fmt.Errorf("failed to parse port configuration from string '%s': %w", *p.StringValue, err)
		}

		return result, nil
	}

	if p.StructValue != nil {
		result := &PortConfiguration{}
		value := p.StructValue

		result.HostPort = value.Published
		result.ContainerPort = value.Target

		if value.Protocol != ProtocolTcp {
			return nil, fmt.Errorf("protocol '%s' is not supported, host port '%d', target port '%d'", value.Protocol, value.Published, value.Target)
		}

		result.Protocol = ProtocolTcp
	}

	return nil, nil
}

func (p *PortConfiguration) Identity() string {
	return fmt.Sprintf("p-%d-%d", p.HostPort, p.ContainerPort)
}

func parsePortConfiguration(portConfiguration string) (*PortConfiguration, error) {
	result := &PortConfiguration{}

	configurationParts := strings.Split(portConfiguration, ":")

	containerPort := ""
	var protocol Protocol = ProtocolTcp

	containerPortWithProtocol := configurationParts[len(configurationParts)-1]
	parts := strings.Split(containerPortWithProtocol, "/")

	switch len(parts) {
	case 1:
		// only port
		containerPort = parts[0]
	case 2:
		// port with protocol
		containerPort = parts[0]
		if parts[1] != ProtocolTcp {
			return nil, fmt.Errorf("protocol '%s' is supported for port '%s'", parts[1], containerPortWithProtocol)
		}

		protocol = Protocol(parts[1])
	default:
		return nil, fmt.Errorf("cannot parse container port with optional protocol '%s', has more than 2 parts", containerPortWithProtocol)
	}

	switch len(configurationParts) {
	case 1:
		target, err := strconv.Atoi(containerPort)
		if err != nil {
			return nil, fmt.Errorf("cannot parse container port '%s'", containerPort)
		}

		result.HostPort = int32(target)
		result.ContainerPort = int32(target)
		result.Protocol = protocol

		return result, nil
	case 2:
		container, err := strconv.Atoi(containerPort)
		if err != nil {
			return nil, fmt.Errorf("cannot parse to int container port '%s'", containerPort)
		}

		host, err := strconv.Atoi(configurationParts[0])
		if err != nil {
			return nil, fmt.Errorf("cannot parse to int host port '%s'", configurationParts[0])
		}

		result.HostPort = int32(host)
		result.ContainerPort = int32(container)
		result.Protocol = protocol

		return result, nil
	case 3:
		container, err := strconv.Atoi(containerPort)
		if err != nil {
			return nil, fmt.Errorf("cannot parse to int container port '%s'", containerPort)
		}

		host, err := strconv.Atoi(configurationParts[1])
		if err != nil {
			return nil, fmt.Errorf("cannot parse to int host port '%s'", configurationParts[1])
		}

		result.HostPort = int32(host)
		result.ContainerPort = int32(container)
		result.Protocol = protocol

		return result, nil
	default:
		return nil, fmt.Errorf("cannot to parse port configuration, contains more than 3 parts")
	}
}
